using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Data;

namespace Sys.DataAccess
{
    public class ObjectConstructionInfo
    {
        public ObjectConstructionInfo(DataTable table, Type objectType)
        {
            Build_ColMap_And_SortedInstantiationList(table, objectType);
        }

        Dictionary<string, ColumnMapEntry> columnMap = new Dictionary<string, ColumnMapEntry>();

        //An object might contain several child objects (which need to be instantiated). 
        //Like the Car class might contain an Engine class. When setting Car.Engine.HorsePower(int),
        //Car.Engine might need to be new()ed, as Car.Engine = new Engine();
        //Hence the instantiation list.
        Dictionary<string, ConstructorInfo> instantiationList = new Dictionary<string, ConstructorInfo>();

        private void Build_ColMap_And_SortedInstantiationList(DataTable table, Type objectType)
        {
            List<string> columnNames = new List<string>();
            foreach (DataColumn column in table.Columns)
            {
                columnNames.Add(column.ColumnName);
            }

            columnNames.Sort();
            foreach (string columnName in columnNames)
            {
                Type objType = objectType;
                ColumnMapEntry entry = new ColumnMapEntry();
                PropertyInfo propInfo = null;
                string[] props = columnName.Split('.');
                for (int i = 0; i < props.Length; i++)
                {
                    propInfo = objType.GetProperty(props[i]);
                    if (propInfo != null)
                    {
                        entry.PropertyInfo = propInfo;
                        if (i < props.Length - 1)
                        {
                            objType = propInfo.PropertyType;
                            string memberName = string.Join(".", props, 0, i + 1);
                            if (!instantiationList.ContainsKey(memberName))
                            {
                                ConstructorInfo constructor = objType.GetConstructor(System.Type.EmptyTypes);
                                instantiationList.Add(memberName, constructor);
                            }
                        }
                    }
                    else break;
                }
                if (propInfo != null)
                {
                    entry.PropertyInfo = propInfo;
                    columnMap[columnName] = entry;
                    if (!columnName.Contains(".")) entry.ContainerName = "";
                    else entry.ContainerName = columnName.Substring(0, columnName.LastIndexOf('.'));
                }
            }
        }


        public object ConstructObject(Type mainObjType, DataRow row, DataColumnCollection columns)
        {
            //Let's create the main object
            object mainObj = mainObjType.GetConstructor(System.Type.EmptyTypes).Invoke(null);
            Dictionary<string, object> constructedObjects = ConstructAndReturnInnerObjects(mainObj);

            foreach (DataColumn column in columns)
            {
                string columnName = column.ColumnName;
                if (columnMap.ContainsKey(columnName))
                {
                    if (row[columnName] != DBNull.Value)
                    {
                        object container;
                        ColumnMapEntry mapEntry = columnMap[column.ColumnName];
                        if (mapEntry.ContainerName == "") container = mainObj;
                        else container = constructedObjects[mapEntry.ContainerName];
                        try
                        {
                            mapEntry.PropertyInfo.SetValue(container, row[column.ColumnName], null);
                        }
                        catch(Exception ex)
                        {
                            string containerName = mapEntry.ContainerName;
                            if (containerName == "") containerName = mainObj.GetType().ToString();
                            string problemText = "Construct Object: Unable to Set Value " + column.ColumnName + " in " + containerName +
                                ". DataType is " + mapEntry.PropertyInfo.PropertyType.ToString() + "." +
                                " MapEntry PropertyInfo target declared in " + mapEntry.PropertyInfo.DeclaringType.ToString() + ".";

                            Exception unableToSet = new Exception(problemText, ex);
                            throw unableToSet;
                        }
                    }
                }
            }
            return mainObj;
        }

        private Dictionary<string, object> ConstructAndReturnInnerObjects(object container)
        {
            object obj;
            Dictionary<string, object> constructedObjects = new Dictionary<string,object>();
            //Note that instantiationList is sorted. This optimization allows us 
            //to construct classes in the order in which they appear in the list.
            //ie, It ensures that the container instance is always already created.
            foreach (KeyValuePair<string, ConstructorInfo> pair in instantiationList)
            {
                obj = container;
                string[] fields = pair.Key.Split('.');
                foreach (string field in fields)
                {
                    PropertyInfo property = obj.GetType().GetProperty(field);
                    if (property.GetValue(obj, null) == null)
                    {
                        property.SetValue(obj, pair.Value.Invoke(System.Type.EmptyTypes), null);
                    }
                    obj = property.GetValue(obj, null);
                    constructedObjects[pair.Key] = obj;
                }
            }
            return constructedObjects;
        }
    }
}

